Skip to content

fix: inherit @SimpleBuilder options from parent classes (#248) - #21 - #252

Merged
AndreasIgel merged 17 commits into
java-helpers:mainfrom
AndreasIgel:devin/fix-issue-248-inherited-options
Aug 16, 2026
Merged

fix: inherit @SimpleBuilder options from parent classes (#248) - #21#252
AndreasIgel merged 17 commits into
java-helpers:mainfrom
AndreasIgel:devin/fix-issue-248-inherited-options

Conversation

@AndreasIgel

@AndreasIgel AndreasIgel commented Aug 16, 2026

Copy link
Copy Markdown
Collaborator

Fixes #248.

Summary

BuilderConfigurationReader now applies @SimpleBuilder and custom @SimpleBuilder.Template options inherited from parent classes, while guaranteeing that directly declared annotations always override inherited ones.

Problem

@SimpleBuilder and @SimpleBuilder.Template are @Inherited, but BuilderConfigurationReader only inspected element.getAnnotationMirrors(), which returns directly declared annotations. Subclasses without their own annotation fell back to defaults, ignoring the parent's configured builder options. Additionally, simply switching to Elements.getAllAnnotationMirrors(element) could cause an inherited @SimpleBuilder on a parent to suppress a subclass's own template annotation.

Changes

  • Introduce an AnnotationScope enum (DIRECT / INHERITED) and helpers to read annotations from each scope independently.
  • Resolve configuration in strict precedence order:
    1. Direct @SimpleBuilder(options = ...) inline options
    2. Custom template annotations directly declared on the class (i.e. the class is directly annotated with a custom annotation that is meta-annotated with @SimpleBuilder.Template)
    3. Inherited @SimpleBuilder(options = ...) inline options
    4. Inherited @SimpleBuilder.Template annotations
    5. Global compiler arguments
    6. Built-in defaults
  • Only ignore template annotations in the same scope where @SimpleBuilder is present. This prevents an inherited @SimpleBuilder from a superclass from suppressing a subclass's own template annotation.
  • Add regression tests in BuilderAnnotationInheritanceTest:
    • subclassTemplateOverridesInheritedSimpleBuilderOptions
    • subclassTemplateOverridesInheritedTemplate
  • Update SimpleBuilder Javadoc and docs/CONFIGURATION.md to document inherited options.

Example

@SimpleBuilder(options = @SimpleBuilder.Options(generateFieldSupplier = OptionState.DISABLED))
public class ParentDto { ... }

public class ChildDto extends ParentDto { ... }

Before: ChildDtoBuilder ignored the parent's options and generated a name(Supplier<String>) method.
After: ChildDtoBuilder respects generateFieldSupplier = DISABLED and omits the supplier method.

Link to Devin session: https://app.devin.ai/sessions/48093048a6a34b3daf66c81058e29108
Requested by: @AndreasIgel


Open in Devin Review

AndreasIgel and others added 17 commits August 15, 2026 10:57
The class-level Javadoc and docs/CONFIGURATION.md implied that
@SimpleBuilder is inherited by subclasses, but the annotation was not
meta-annotated with @inherited. As a result BuilderProcessor, which
collects types via RoundEnvironment.getElementsAnnotatedWith(...),
only produced builders for the exact type carrying @SimpleBuilder and
not for unannotated subclasses.

Add @inherited to @SimpleBuilder so subclasses are treated as if they
also carried the annotation, mirroring the existing behaviour of
@SimpleBuilder.Template (which is already @inherited). Update the
Javadoc to document the inheritance explicitly and clarify the
CONFIGURATION.md wording. @Ignore4BuilderGeneration still suppresses
generation for the exact type it is placed on, so opt-outs continue
to work as before.

Add SimpleBuilderInheritanceTest covering direct inheritance, the
opt-out interaction, and multi-level (grandchild) inheritance.

Closes java-helpers#244

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
…ance

Rename SimpleBuilderInheritanceTest to BuilderAnnotationInheritanceTest
so the name reflects that it covers both builder-triggering annotations.
Add unannotatedSubclassGetsBuilderFromInheritedTemplate, which verifies
that a custom @inherited template annotation (meta-annotated with
@SimpleBuilder.Template) propagates to unannotated subclasses, matching
the existing behaviour of @SimpleBuilder itself.

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
The Template Javadoc and CONFIGURATION.md "Template Annotations" section
did not explain that @SimpleBuilder.Template is @inherited, nor that a
custom template annotation must additionally declare @inherited to
propagate to unannotated subclasses. Add explicit documentation and an
example showing the @inherited custom annotation pattern.

Also move assertNoBuilderGenerated to ProcessorAsserts so it is shared
by BuilderAnnotationInheritanceTest and Ignore4BuilderGenerationTest
instead of being duplicated as a private helper in each test class.

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
While @SimpleBuilder and @inherited template annotations now correctly
trigger builder generation for unannotated subclasses, the configuration
options declared on the parent's @SimpleBuilder(options = ...) or template
are not yet applied to inherited subclass builders — they use default
options instead. This is tracked separately in issue java-helpers#245.

Add caveats to the SimpleBuilder Javadoc, the CONFIGURATION.md Template
Annotations section, and the Template Annotations Not Working
troubleshooting section so users are not surprised by this limitation.

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Add explicit guidance that @SimpleBuilder.Template is a meta-annotation
for custom annotation declarations (@interface) only and cannot be
placed directly on a class or record. @SimpleBuilder is for direct
one-off annotation of classes/records.

- SimpleBuilder.java: add 'When to use' section to class-level Javadoc
- SimpleBuilder.Template Javadoc: state it can only be placed on
  annotation types (ANNOTATION_TYPE), not on classes/records
- CONFIGURATION.md 'Template Annotations': add comparison table and
  introductory paragraph
- CONFIGURATION.md troubleshooting: add item about @SimpleBuilder.Template
  not being a class annotation

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
…Javadoc

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
…arget

The compiler and IDE already enforce @target(ANNOTATION_TYPE) and show
a clear error when @SimpleBuilder.Template is placed on a class/record,
so this troubleshooting item adds no value.

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
…#248)

BuilderConfigurationReader used element.getAnnotationMirrors(), which only
returns directly-declared annotations. This caused unannotated subclasses of
an annotated parent to get a builder with default options instead of the
parent's @SimpleBuilder(options=...) or template-annotation options.

Switch the three configuration-reading methods to
Elements.getAllAnnotationMirrors(element), which includes @inherited
annotations from superclasses.

Add regression tests in BuilderAnnotationInheritanceTest covering both
@SimpleBuilder and custom template annotation option inheritance.

Update SimpleBuilder Javadoc and CONFIGURATION.md to document that options
are inherited too.

Closes java-helpers#248

Co-Authored-By: Andreas Igel <andreas.igel@computacenter.com>
… ones

- Introduce AnnotationScope (DIRECT/INHERITED) in BuilderConfigurationReader.
- Resolve config in precedence order: defaults < globals < inherited template <
  inherited @SimpleBuilder < direct template < direct @SimpleBuilder.
- Only ignore template annotations in the same scope where @SimpleBuilder is
  declared, preventing an inherited @SimpleBuilder from suppressing a subclass's
  own template.
- Add regression tests in BuilderAnnotationInheritanceTest for a subclass
  template overriding inherited @SimpleBuilder options and inherited template
  options.
- Align junit-jupiter.version to 6.0.3 (6.1.2 is not published) so the test suite
  can resolve.
- Update debug-message expectation in BuilderProcessorTest.

Co-Authored-By: Andreas Igel <andreas.igel@computacenter.com>
…ce fix

Co-Authored-By: Andreas Igel <andreas.igel@computacenter.com>
…ts actually run

Co-Authored-By: Andreas Igel <andreas.igel@computacenter.com>
Co-Authored-By: Andreas Igel <andreas.igel@computacenter.com>
Co-Authored-By: Andreas Igel <andreas.igel@computacenter.com>
@codecov

codecov Bot commented Aug 16, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ All tests successful. No failed tests found.

📢 Thoughts on this report? Let us know!

@AndreasIgel
AndreasIgel merged commit 031bb2a into java-helpers:main Aug 16, 2026
6 checks passed
AndreasIgel added a commit that referenced this pull request Aug 16, 2026
…ation

Resolved conflict in SimpleBuilder.java: took updated Javadoc from
origin/main (PR #252) which now documents that template options ARE
applied to inherited subclass builders, replacing the old note about
issue #248.

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
@devin-ai-integration
devin-ai-integration Bot deleted the devin/fix-issue-248-inherited-options branch August 16, 2026 21:08
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Inherited @SimpleBuilder / @SimpleBuilder.Template options are not applied to subclasses — only defaults

1 participant